﻿//-----------------------------------------------------------------------------
// <copyright file="View.cs" company="William E. Kempf">
//     Copyright (c) William E. Kempf.
// </copyright>
//-----------------------------------------------------------------------------

namespace Onyx.Windows
{
    using System;
    using System.Diagnostics.CodeAnalysis;
#if SILVERLIGHT
    using System.Reflection;
#endif
    using System.Windows;
    using System.ComponentModel;
    using Onyx.Reflection;
using Onyx.ComponentModel;

    /// <summary>
    /// Represents a view in the M-V-VM pattern.
    /// </summary>
    [SuppressMessage("Microsoft.Design",
        "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable",
        Justification = "User's not really going to be able to dispose of the Views.")]
    public sealed class View : IServiceProvider
    {
        /// <summary>
        /// Defines the "Model" attached dependency property.
        /// </summary>
#if WPF
        public static readonly DependencyProperty ModelProperty =
            DependencyProperty.RegisterAttached(
                "Model",
                typeof(object),
                typeof(View),
                new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.Inherits, OnModelChanged, CoerceModel));
#endif
#if SILVERLIGHT
        public static readonly DependencyProperty ModelProperty =
            DependencyProperty.RegisterAttached(
                "Model",
                typeof(object),
                typeof(View),
                new PropertyMetadata(null, OnModelChanged));
#endif

        /// <summary>
        /// Defines the "Name" attached dependency property.
        /// </summary>
        public static readonly DependencyProperty NameProperty =
            DependencyProperty.RegisterAttached(
                "Name",
                typeof(string),
                typeof(View),
                new PropertyMetadata(null));

        /// <summary>
        /// Defines the "View" attached dependency property.
        /// </summary>
        private static readonly DependencyProperty ViewProperty =
            DependencyProperty.RegisterAttached(
                "ViewInternal",
                typeof(View),
                typeof(View),
                new PropertyMetadata(null));

        /// <summary>
        /// The view element associated with this <see cref="View"/>.
        /// </summary>
        private readonly DependencyObject viewElement;

        /// <summary>
        /// The <see cref="IServiceProvider"/> associated with this instance.
        /// </summary>
        private IServiceProvider serviceContainer;

        /// <summary>
        /// The view model activator to use.
        /// </summary>
        private static IViewModelActivator viewModelActivator;

        private static IServiceRegistryActivator serviceRegistryActivator;

        /// <summary>
        /// An object to used for synchronization.
        /// </summary>
        private static object sync = new object();

        /// <summary>
        /// Initializes a new instance of the <see cref="View"/> class.
        /// </summary>
        /// <param name="viewElement">The view element.</param>
        private View(DependencyObject viewElement)
        {
            this.viewElement = viewElement;
        }

        /// <summary>
        /// Occurs when a <see cref="View"/> is created.
        /// </summary>
        /// <remarks>
        /// This event can be subscribed to in order to add services to the created <see cref="View"/>.
        /// </remarks>
        public static event EventHandler<ViewCreatedEventArgs> ViewCreated = OnViewCreated;

        /// <summary>
        /// Gets a value indicating whether the application is running in design mode.
        /// </summary>
        /// <value>
        /// <see langword="true"/> if the application is in design mode; otherwise, <see langword="false"/>.
        /// </value>
        public static bool IsInDesignMode
        {
            get
            {
                return (null == Application.Current) || (Application.Current.GetType() == typeof(Application));
            }
        }

        /// <summary>
        /// Gets or sets the view model activator.
        /// </summary>
        /// <value>The view model activator.</value>
        /// <remarks>
        /// This provides a hook for using an IoC container to create view model instances.
        /// </remarks>
        public static IViewModelActivator ViewModelActivator
        {
            get
            {
                if (View.viewModelActivator == null)
                {
                    lock (View.sync)
                    {
                        if (View.viewModelActivator == null)
                        {
                            View.viewModelActivator = new SimpleViewModelActivator();
                        }
                    }
                }

                return View.viewModelActivator;
            }

            set
            {
                lock (View.sync)
                {
                    if (View.viewModelActivator != null)
                    {
                        throw new InvalidOperationException("Already assigned"); // TODO: better exception
                    }

                    View.viewModelActivator = value;
                }
            }
        }

        public static IServiceRegistryActivator ServiceRegistryActivator
        {
            get
            {
                if (View.serviceRegistryActivator == null)
                {
                    lock (View.sync)
                    {
                        if (View.serviceRegistryActivator == null)
                        {
                            View.serviceRegistryActivator = new OnyxServiceRegistryActivator();
                        }
                    }
                }

                return View.serviceRegistryActivator;
            }

            set
            {
                lock (View.sync)
                {
                    if (View.serviceRegistryActivator != null)
                    {
                        throw new InvalidOperationException("Already assigned"); // TODO: better exception
                    }

                    View.serviceRegistryActivator = value;
                }
            }
        }

        /// <summary>
        /// Gets the view element associated with this <see cref="View"/>.
        /// </summary>
        /// <value>The view element associated with this <see cref="View"/>.</value>
        /// <exception cref="ObjectDisposedException">This instance has been disposed.</exception>
        public DependencyObject ViewElement
        {
            get
            {
                return this.viewElement;
            }
        }

        /// <summary>
        /// Gets the view.
        /// </summary>
        /// <param name="source">The source.</param>
        /// <returns>
        /// The <see cref="View"/> associated with the <paramref name="source"/>.
        /// </returns>
        /// <remarks>
        /// If there currently is no <see cref="View"/> associated with the <paramref name="source"/>
        /// then a new <see cref="View"/> is created.
        /// </remarks>
        /// <exception cref="ArgumentNullException"><paramref name="source"/> is null.</exception>
        public static View GetView(DependencyObject source)
        {
            if (source == null)
            {
                throw new ArgumentNullException(Reflect.GetField(() => source).Name);
            }

            View view = (View)source.GetValue(ViewProperty);
            if (view == null)
            {
                view = new View(source);
                source.SetValue(ViewProperty, view);
                IServiceRegistry container = View.ServiceRegistryActivator.CreateServiceRegistry();
                View.ViewCreated(view, new ViewCreatedEventArgs(view.ViewElement, container));
                container.RegisterServicesFor(view.ViewElement);
                view.serviceContainer = container.CreateServiceProvider();
            }

            return view;
        }

        /// <summary>
        /// Gets the ViewModel associated with <paramref name="source"/>.
        /// </summary>
        /// <param name="source">The source.</param>
        /// <returns>
        /// The ViewModel associated with <paramref name="source"/>.
        /// </returns>
        /// <exception cref="ArgumentNullException"><paramref name="source"/> is null.</exception>
        public static object GetModel(DependencyObject source)
        {
            if (source == null)
            {
                throw new ArgumentNullException(Reflect.GetField(() => source).Name);
            }

            return source.GetValue(View.ModelProperty);
        }

        /// <summary>
        /// Sets the ViewModel associated with <paramref name="source"/>.
        /// </summary>
        /// <param name="source">The source.</param>
        /// <param name="value">The ViewModel to associate with <paramref name="source"/>.</param>
        /// <exception cref="ArgumentNullException"><paramref name="source"/> is null.</exception>
        public static void SetModel(DependencyObject source, object value)
        {
            if (source == null)
            {
                throw new ArgumentNullException(Reflect.GetField(() => source).Name);
            }

            source.SetValue(View.ModelProperty, value);
        }

        /// <summary>
        /// Gets the view name associated with <paramref name="source"/>.
        /// </summary>
        /// <param name="source">The source.</param>
        /// <returns>
        /// The view name associated with <paramref name="source"/>.
        /// </returns>
        /// <exception cref="ArgumentNullException"><paramref name="source"/> is null.</exception>
        public static string GetName(DependencyObject source)
        {
            if (source == null)
            {
                throw new ArgumentNullException(Reflect.GetField(() => source).Name);
            }

            return (string)source.GetValue(View.NameProperty);
        }

        /// <summary>
        /// Sets the view name associated with <paramref name="source"/>.
        /// </summary>
        /// <param name="source">The source.</param>
        /// <param name="name">The view name.</param>
        /// <exception cref="ArgumentNullException"><paramref name="source"/> is null.</exception>
        public static void SetName(DependencyObject source, string name)
        {
            if (source == null)
            {
                throw new ArgumentNullException(Reflect.GetField(() => source).Name);
            }

            source.SetValue(View.NameProperty, name);
        }

        /// <summary>
        /// Gets the service object of the specified type.
        /// </summary>
        /// <param name="serviceType">An object that specifies the type of service object to get.</param>
        /// <returns>
        /// A service object of type <paramref name="serviceType"/>.
        /// -or-
        /// null if there is no service object of type <paramref name="serviceType"/>.
        /// </returns>
        /// <exception cref="ObjectDisposedException">This instance has been disposed.</exception>
        public object GetService(Type serviceType)
        {
            if (serviceType == null)
            {
                throw new ArgumentNullException(Reflect.GetField(() => serviceType).Name);
            }

            return this.serviceContainer.GetService(serviceType);
        }

        /// <summary>
        /// Called when the <see cref="ModelProperty"/> attached dependency property is changed on the <paramref name="source"/>.
        /// </summary>
        /// <param name="source">The source object.</param>
        /// <param name="e">The <see cref="DependencyPropertyChangedEventArgs"/> isntance containing the event data.</param>
        private static void OnModelChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
        {
            if (source.AsFrameworkObject() == null)
            {
                return;
            }

            object model = e.NewValue;
            if (model == null)
            {
                return;
            }

#if WPF
            if (DependencyPropertyHelper.GetValueSource(source, ModelProperty).BaseValueSource == BaseValueSource.Local &&
                DependencyPropertyHelper.GetValueSource(source, FrameworkElement.DataContextProperty).BaseValueSource != BaseValueSource.Local)
            {
                source.SetValue(FrameworkElement.DataContextProperty, model);
            }
#endif
#if SILVERLIGHT
            if (model is string)
            {
                Assembly assembly = Application.Current.GetType().Assembly;
                if (assembly == null)
                {
                    throw new ArgumentNullException(Reflect.GetField(() => assembly).Name);
                }

                Type type = assembly.GetType(model.ToString(), true);

                View view = GetView(source);
                model = Activator.CreateInstance(type, view);
            }

            source.SetValue(FrameworkElement.DataContextProperty, model);
#endif
        }

        /// <summary>
        /// Coerces the model.
        /// </summary>
        /// <param name="source">The source.</param>
        /// <param name="baseValue">The base value.</param>
        /// <returns>
        /// The coerced model value.
        /// </returns>
        private static object CoerceModel(DependencyObject source, object baseValue)
        {
            Type type = baseValue as Type;
            if (type != null)
            {
                return View.ViewModelActivator.CreateViewModel(type, View.GetView(source)) ?? baseValue;
            }

            return baseValue;
        }

        /// <summary>
        /// Called when a <see cref="View"/> is created.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="ViewCreatedEventArgs"/> instance containing the event data.</param>
        /// <remarks>
        /// Adds common framework services to the <see cref="View"/>.
        /// </remarks>
        private static void OnViewCreated(object sender, ViewCreatedEventArgs e)
        {
            UIElement uiElement = e.ViewElement as UIElement;
            if (uiElement != null)
            {
                UIElementServices services = new UIElementServices(uiElement);
                services.AddServices(e.ServiceContainer);
            }

            IFrameworkObject frameworkObject = e.ViewElement.AsFrameworkObject();
            if (frameworkObject != null)
            {
                CommonFrameworkElementServices services = new CommonFrameworkElementServices(frameworkObject);
                services.AddServices(e.ServiceContainer);
            }
        }
    }
}
